Skip to main content

Code Quality & Linting

Multiple layers catch issues at different stages of development. Earlier layers provide faster feedback; later layers ensure nothing slips through.

Defense in Depth: Three Layers

┌─────────────────────────────────────────────────────────────────┐
│ LAYER 1: IDE / Agent Hooks │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ • Editor on-save (VSCode/Cursor/etc formatOnSave) │ │
│ │ • Claude Code and other agent's hooks │ │
│ │ When: As you type/save │ │
│ │ Catches: Format issues, syntax errors │ │
│ │ Status: PLANNED │ │
│ └───────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 2: Git Hooks │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ • Pre-commit: auto-fix staged files │ │
│ │ • Pre-push: validate without fixing │ │
│ │ When: Before code leaves local machine │ │
│ │ Catches: Lint errors, format issues │ │
│ │ Status: ACTIVE │ │
│ └───────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 3: CI Pipelines │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ • GitLab CI jobs on MR/push │ │
│ │ When: Code reaches remote repository │ │
│ │ Catches: Everything (full repo scan) │ │
│ │ Status: ACTIVE │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘


↓ Feedback Speed: Instant → Seconds → Minutes
↓ Scope: Single file → Changed files → Full repo


Layer 1: IDE/Agent Hooks (Planned)

Coming Soon

This layer will provide instant feedback as you code. It will cover:

  • Editor on-save hooks: VSCode/Cursor formatOnSave and codeActionsOnSave
  • Claude Code hooks: Automated formatting/linting via Claude's hook system

Layer 2: Git Hooks

Git hooks catch issues before code leaves your machine. We use Lefthook to run formatters and linters on staged/pushed files.

Installation

Hooks run outside containers, so they need to be installed on your host machine.

First, ensure you have Lefthook installed:

brew install lefthook

Then run per repository:

lefthook install

Tools

ToolPurposeNotes
LefthookHook runnerConsistent behavior, deep customization. Install via brew, not npm
DusterPHP lint/formatDefault toolset (TLint, PHPCS, PHP-CS-Fixer, Pint). No duster.json needed
Larastan/PHPStanStatic analysisLaravel rules on top of PHPStan. Level 6 default if no config
ESLintJS lintingStable ecosystem. May migrate to biome/oxc later
PrettierFormattingStable, widely adopted. May migrate to biome/oxc later
tscTypeScript typecheckPre-push in TS repos

Rules (Apply Everywhere)

  • Hooks use glob: filters, {staged_files} (pre-commit), {push_files} (pre-push)
  • Container repos: guard with zoo check <service> and run via zex ...
  • Non-container repos: run via local npx ...
  • No wrapper scripts. Run tools directly
  • CI runs tools directly on full repo

Hook Strategy

We use a two-phase approach: pre-commit auto-fixes your code as you commit (fast feedback on staged files only), while pre-push validates without fixing (catches what slipped through and blocks bad pushes). This keeps commits clean while ensuring nothing broken reaches the remote.

Pre-commit

Runs formatters and fixers on staged files. Fixed files are auto-staged.

  • JS/TS: prettier --write then eslint --fix || true
  • PHP: duster fix (on failure, run duster lint for details)

Pre-push

Validates without modifying. Blocks push on failure.

  • JS/TS: eslint (no --fix)
  • PHP: phpstan analyse

See Linting References for complete config examples.

Hook UX Patterns

  • Progress messages: echo "Running X..." before each tool for visibility
  • Graceful skip: if container not running, warn and exit 0 (CI will catch issues)
  • Fail with details: on duster fix failure, run lint to show what's wrong

Layer 3: CI Pipelines

CI is the final safety net - it runs on every push and MR, scanning the entire repo regardless of what was changed locally.

LanguageJobsCommands
JSlint:jseslint ., prettier --check .
PHPlint:php, analyze:static:phpstan./vendor/bin/duster lint, ./vendor/bin/phpstan analyse
  • CI never relies on hooks - it runs tools directly on the full repo

Disabling Hooks Temporarily

# Disable all hooks
rm -f .git/hooks/*

# Re-enable
lefthook install

Exemplars

See Linting References for repo-specific configurations and examples.

X

Graph View